/** * Copyright (C) 2013 Johannes Schnatterer * * See the NOTICE file distributed with this work for additional * information regarding copyright ownership. * * This file is part of nusic. * * nusic is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * nusic is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with nusic. If not, see <http://www.gnu.org/licenses/>. */ package info.schnatterer.nusic.android.util; import info.schnatterer.nusic.ui.R; import java.io.IOException; import java.io.InputStream; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.Stack; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.apache.commons.io.IOUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.xml.sax.XMLReader; import android.content.Context; import android.text.Editable; import android.text.Html; import android.text.Spanned; import android.text.style.BulletSpan; import android.text.style.LeadingMarginSpan; import android.util.Log; public class TextUtil { private static final Logger LOG = LoggerFactory.getLogger(TextUtil.class); /** * Regex that matches a resource string such as <code>@string/a-b_c1</code>. */ private static final String REGEX_RESOURCE_STRING = "@string/([A-Za-z0-9-_]*)"; /** * Regex that matches file names case insensitively that have common file * extensions for HTML. */ private static final String REGEX_ENDING_HTML = "(?i)^.*(\\.htm[l]?)$"; private TextUtil() { } /** * Tries to load an asset file as text. If <code>assetPath</code> ends in * <code>.html</code>, the HTML code is rendered into "displayable styled" * text. * * @param context * context to load asset and (potential resources) from * @param assetPath * path of the asset to load * @return (potentially styled) text from asset */ public static CharSequence loadTextFromAsset(Context context, String assetPath) { if (assetPath != null) { InputStream is = null; try { is = context.getResources().getAssets().open(assetPath); String assetText = IOUtils.toString(is); if (assetPath.matches(REGEX_ENDING_HTML)) { return fromHtml(replaceResourceStrings(context, assetText)); } else { return assetText; } } catch (IOException e) { LOG.warn( "Unable to load asset from path \"" + assetPath + "\"", e); return context .getString(R.string.TextAssetActivity_errorLoadingFile); } finally { IOUtils.closeQuietly(is); } } return null; } /** * Tries to load an asset file as text. If <code>assetPath</code> ends in * <code>.html</code>, the HTML code is rendered into "displayable styled" * text. * * @param context * context to load asset and (potential resources) from * @param assetPath * path of the asset to load * @return text from asset */ public static String loadTextFromAssetAsString(Context context, String assetPath) { if (assetPath != null) { InputStream is = null; try { is = context.getResources().getAssets().open(assetPath); return IOUtils.toString(is); } catch (IOException e) { LOG.warn( "Unable to load asset from path \"" + assetPath + "\"", e); return context .getString(R.string.TextAssetActivity_errorLoadingFile); } finally { IOUtils.closeQuietly(is); } } return null; } /** * Recursively replaces resources such as <code>@string/abc</code> with * their localized values from the app's resource strings (e.g. * <code>strings.xml</code>) within a <code>source</code> string. * * Also works recursively, that is, when a resource contains another * resource that contains another resource, etc. * * @param context * @param source * @return <code>source</code> with replaced resources (if they exist) */ public static String replaceResourceStrings(Context context, String source) { // Recursively resolve strings Pattern p = Pattern.compile(REGEX_RESOURCE_STRING); Matcher m = p.matcher(source); StringBuffer sb = new StringBuffer(); while (m.find()) { String stringFromResources = ResourceUtil.getStringByName(context, m.group(1)); if (stringFromResources == null) { LOG.warn("No String resource found for ID \"" + m.group(1) + "\" while inserting resources"); /* * No need to try to load from defaults, android is trying that * for us. If we're here, the resource does not exist. Just * return its ID. */ stringFromResources = m.group(1); } m.appendReplacement(sb, // Recurse replaceResourceStrings(context, stringFromResources)); } m.appendTail(sb); return sb.toString(); } /** * Similar to {@link android.text.Html#fromHtml(String)}, but provides * support for more HTML-tags such as lists. * * @param string * @return */ public static CharSequence fromHtml(String string) { return Html.fromHtml(string.replaceFirst("<title>.*</title>", ""), null, new MyTagHandler()); } /** * Copyright (C) 2013-2014 Juha Kuitunen Copyright (C) 2007 The Android Open * Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); you may * not use this file except in compliance with the License. You may obtain a * copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations * under the License. */ /** * Implements support for ordered and unordered lists in to Android * TextView. * * Some code taken from inner class * android.text.Html.HtmlToSpannedConverter. If you find this code useful, * please vote my answer at <a * href="http://stackoverflow.com/a/17365740/262462">StackOverflow</a> up. * * @version v1.0.1 */ private static class MyTagHandler implements Html.TagHandler { /** * A list of tags that do not influence rendering. We don't want to have * them on the log as unsupported tags. */ protected static final Set<String> IGNORED_TAGS = Collections.unmodifiableSet( new HashSet<>(Arrays.asList("html", "head", "body"))); /** /** * Keeps track of lists (ol, ul). On bottom of Stack is the outermost * list and on top of Stack is the most nested list */ Stack<String> lists = new Stack<String>(); /** * Tracks indexes of ordered lists so that after a nested list ends we * can continue with correct index of outer list */ Stack<Integer> olNextIndex = new Stack<Integer>(); /** * List indentation in pixels. Nested lists use multiple of this. */ private static final int indent = 10; private static final int listItemIndent = indent * 2; private static final BulletSpan bullet = new BulletSpan(indent); @Override public void handleTag(boolean opening, String tag, Editable output, XMLReader xmlReader) { if (tag.equalsIgnoreCase("ul")) { if (opening) { lists.push(tag); } else { lists.pop(); } } else if (tag.equalsIgnoreCase("ol")) { if (opening) { lists.push(tag); // TODO: add support for lists starting other index than 1 olNextIndex.push(Integer.valueOf(1)).toString(); } else { lists.pop(); olNextIndex.pop().toString(); } } else if (tag.equalsIgnoreCase("li")) { if (opening) { if (output.length() > 0 && output.charAt(output.length() - 1) != '\n') { output.append("\n"); } String parentList = lists.peek(); if (parentList.equalsIgnoreCase("ol")) { start(output, new Ol()); output.append(olNextIndex.peek().toString() + ". "); olNextIndex.push(Integer.valueOf(olNextIndex.pop() .intValue() + 1)); } else if (parentList.equalsIgnoreCase("ul")) { start(output, new Ul()); } } else { if (lists.peek().equalsIgnoreCase("ul")) { if (output.charAt(output.length() - 1) != '\n') { output.append("\n"); } // Nested BulletSpans increases distance between bullet // and text, so we must prevent it. int bulletMargin = indent; if (lists.size() > 1) { bulletMargin = indent - bullet.getLeadingMargin(true); if (lists.size() > 2) { // This get's more complicated when we add a // LeadingMarginSpan into the same line: // we have also counter it's effect to // BulletSpan bulletMargin -= (lists.size() - 2) * listItemIndent; } } BulletSpan newBullet = new BulletSpan(bulletMargin); end(output, Ul.class, new LeadingMarginSpan.Standard( listItemIndent * (lists.size() - 1)), newBullet); } else if (lists.peek().equalsIgnoreCase("ol")) { if (output.charAt(output.length() - 1) != '\n') { output.append("\n"); } int numberMargin = listItemIndent * (lists.size() - 1); if (lists.size() > 2) { // Same as in ordered lists: counter the effect of // nested Spans numberMargin -= (lists.size() - 2) * listItemIndent; } end(output, Ol.class, new LeadingMarginSpan.Standard( numberMargin)); } } } else { if (opening) { // Log unknown tags if (!IGNORED_TAGS.contains(tag)) Log.d("TagHandler", "Found an unsupported tag " + tag); } } } /** @see android.text.Html */ private static void start(Editable text, Object mark) { int len = text.length(); text.setSpan(mark, len, len, Spanned.SPAN_MARK_MARK); } /** @see android.text.Html */ private static void end(Editable text, Class<?> kind, Object... replaces) { int len = text.length(); Object obj = getLast(text, kind); int where = text.getSpanStart(obj); text.removeSpan(obj); if (where != len) { for (Object replace : replaces) { text.setSpan(replace, where, len, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } } return; } /** @see android.text.Html */ private static Object getLast(Spanned text, Class<?> kind) { /* * This knows that the last returned object from getSpans() will be * the most recently added. */ Object[] objs = text.getSpans(0, text.length(), kind); if (objs.length == 0) { return null; } return objs[objs.length - 1]; } private static class Ul { } private static class Ol { } } }